package corewarsxxi.logic;

import config.CoreConfig;
import corewarsxxi.logic.throwables.warriorHasNoProcesses;
import corewarsxxi.logic.throwables.simulationOver;
import java.awt.Color;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;

/**
 * Core contains Warriors and the MemoryArray in which they will be placed in.
 * <p>Core is Singleton which means it can be accessed directly from other objects.</p>
 *
 * <p>It is used to perform and handle the game simulation for a given configuration.</p>
 *
 * @version 1.0
 * @author  Luís Fonseca, ei10139 @ FEUP
 */
public class Core {

    /** The colors which Warriors can assume.*/
    private final Color colors[] = new Color[] {
        new Color(0.2f,  0.2f,  1.0f),
        new Color(1.0f,  0.2f,  0.2f),
        new Color(0.0f,  0.59f, 0.0f),
        new Color(0.2f,  0.92f, 1.0f),
        new Color(0.87f, 0.2f,  0.0f),
        new Color(1.0f,  0.4f,  0.0f),
        new Color(1.0f,  0.40f, 0.0f),
        new Color(0.5f,  0.0f,  .25f)};
    private LinkedList<Warrior> warriors;
    private List<Warrior> warriorsList;
    private LinkedList<Warrior> warriorsLost;
    private CoreConfig cc;
    private int performedCycles;
    private ArrayList<IWarriorDeathListener> iwdlList;

    /** The Cores Memory Array as it is.*/
    public MemoryArray memArray;

    /** The currentWarrior that is being proccessed/cycled.*/
    public Warrior currentWarrior;

    /**
     * Contains the results of the simulation.
     * It is usually used when the simulation has come to an end.
     *
     * @since 1.0
     * @author Luís Fonseca, ei10139 @ FEUP
     */
    public class SimulationResults
    {
        private Warrior wVictorious = null;
        private Warrior[] wDead, wStillAlive;

        /**
         * The only constructor which requires all data to be set upfront.
         * @param wVictorious   Warrior that is victorious (can be set as null).
         * @param wDead         Warriors that died (can be set as null).
         * @param wStillAlive   Warriors that are alive (can be set as null).
         */
        public SimulationResults(Warrior wVictorious,
                                 Warrior[] wDead,
                                 Warrior[] wStillAlive)
        {
            this.wVictorious = wVictorious;
            this.wDead = wDead;
            this.wStillAlive = wStillAlive;
        }

        /**
         * Checks the existence of a Winner.
         * @return <code>true</code> if has one.<p><code>false</code> if not.</p>
         */
        public boolean hasWinner()
        {
            return this.wVictorious != null;
        }

        /**
         * Returns the Simulation Winner.
         * @return the Warrior (can be null).
         */
        public Warrior getWinner()
        {
            return this.wVictorious;
        }

        /**
         * Returns the Simulation Warriors that died.
         * @return the Warriors (can be null).
         */
        public Warrior[] getDead()
        {
            return this.wDead;
        }

        /**
         * Returns the Simulation Warriors that are alive.
         * @return the Warriors (can be null).
         */
        public Warrior[] getStillAlive()
        {
            return this.wStillAlive;
        }
    }

    /**
     * Clears the Core Memory Array and Warriors.
     * Should be used before starting a new simulation.
     */
    public void reset()
    {
        this.memArray.reset();
        this.warriors.clear();
        this.warriorsLost.clear();
        this.performedCycles = 0;
    }

    /** Singleton: Using solution of Bill Pugh. */
    private static class CoreHolder {
        public static final Core instance = new Core();
    }

    /**
     * Returns the unique static instance of the Core.
     * @return the Core.
     */
    public static Core getInstance()
    {
        return CoreHolder.instance;
    }

    /**
     * Public constructor of the Core.
     * @param size  Memory Size to initial start with.
     */
    public Core(int size)
    {
        this.memArray = new MemoryArray(size);
        this.warriors = new LinkedList<Warrior>();
        this.warriorsLost = new LinkedList<Warrior>();
        this.cc = new CoreConfig(size, 20000, 200, 100);
        this.performedCycles = 0;
        this.iwdlList = new ArrayList<IWarriorDeathListener>();
    }

    /**
     * Public constructor of the Core with a Memory Size of 8000 cells.
     */
    public Core()
    {
        this(8000);
    }

    /**
     * Loads the Warriors to the Core and sets the game ready state to perform
     * simulations.
     * <p>Warriors are randomly loaded in to the Memory Array, respecting the minimum
     * distance as defined on the CoreConfig.</p>
     * <p>Warriors Colors are assigned at this point.</p>
     * @param ws the Warriors. If <code>ws</code> is null or empty, the currently
     * loaded warriors are unloaded.
     */
    public void loadWarriors(Warrior[] ws)
    {
        this.warriors.clear();
        if(ws == null)
            return;

        if(ws.length == 0)
            return;
        
        Random r = new Random();
        int minD = this.cc.getMinDistance();
        int maxD = (this.getSize() - ws.length * this.cc.getWarriorMaxInstructions()) / ws.length - minD;

        int index = 0, colorIndex = 0;
        warriorsList = Arrays.asList(ws);
        Collections.shuffle(warriorsList, r);
        for(Warrior w : warriorsList)
        {
            index += r.nextInt(maxD);
            this.warriors.add(w);
            if(colorIndex < colors.length)
                w.setColor(colors[colorIndex++]);
            else
                w.setColor(new Color(r.nextInt(255), r.nextInt(255), r.nextInt(255)));

            this.currentWarrior = w;
            MemoryArray.copy(w.program, this.memArray, index);

            // Start the warriors process
            w.processes.add(new Process(index));
            index += minD;
        }
    }

    /**
     * Loads one Warrior to the first cell of the Memory.
     * <p>This function is useful for testing but shouldn't be called elsewhere.</p>
     * @param w the Warrior.
     * @deprecated
     */
    @Deprecated
    public void loadWarrior(Warrior w)
    {
        this.warriors.clear();
        this.warriors.add(w);
        MemoryArray.copy(w.program, this.memArray, 0);
        w.processes.add(new Process(0));
    }

    /**
     * Instantly performs all computatons necessary until the game reaches an end.
     * @return the results of the simulation
     */
    public SimulationResults performFullSimulation()
    {
        // TODO: [Optimization] Perhaps disable notifiers and only call them at the end.
        Warrior wVictorious = null;
        try {
            for(int i = performedCycles; i < cc.getMaxCycles(); ++i)
                this.cycle();
            
        } catch (simulationOver ex) {
            wVictorious = warriors.peek();
        }

        return new SimulationResults(wVictorious,
                                     (Warrior[]) warriorsLost.toArray(new Warrior[0]),
                                     (Warrior[]) warriors.toArray(new Warrior[0]));
    }

    /**
     * Performs a cycle on the next warrior.
     * @throws simulationOver   throws if game has ended or has reached the maxCycles
     */
    public void cycle() throws simulationOver
    {
        if(performedCycles > cc.getMaxCycles())
            throw new simulationOver(new SimulationResults(
                                      warriors.size() == 1? warriors.peek() : null,
                                     (Warrior[]) warriorsLost.toArray(new Warrior[0]),
                                     (Warrior[]) warriors.toArray(new Warrior[0])));

        try {
            currentWarrior = warriors.peek();
            currentWarrior.cycle();
            warriors.add(warriors.remove());
            ++performedCycles;
            //System.out.println("Warrior '" + currentWarrior.getName() + "' has performed a cycle.");
        } catch(warriorHasNoProcesses ex)
        {
            //System.out.println("Warrior '" + currentWarrior.getName() + "' has lost all processes, so it is now dead.");
            warriorsLost.add(currentWarrior);
            warriors.remove();
            for(IWarriorDeathListener list : iwdlList)
            {
                try { list.handle(currentWarrior); }
                catch(Exception e) {}
            }

            if(warriors.size() == 1)
                throw new simulationOver(new SimulationResults(warriors.peek(),
                                     (Warrior[]) warriorsLost.toArray(new Warrior[0]),
                                     (Warrior[]) warriors.toArray(new Warrior[0]))); // and we have a winner!
            
            this.cycle();
        }
    }

    /**
     * Returns the number of Warriors in the Core
     * @return the number of Warriors in the Core
     */
    public int warriorCount()
    {
        return this.warriors.size();
    }

    /**
     * Returns the size of the memory in the Core
     * @return the size of the memory in the Core
     */
    public int getSize()
    {
        return this.memArray.size();
    }

    /**
     * @return the CoreConfig
     */
    public CoreConfig getCoreConfig() {
        return cc;
    }

    /**
     * @param CoreConfig the CoreConfig to set on the Core
     */
    private void setCoreConfig(CoreConfig CoreConfig) {
        this.cc = CoreConfig;
    }

    /**
     * Adds a listener to the Core so when a Warrior dies the IWarriorDeathListener.handler gets called.
     * @param listener the implementation of the IWarriorDeathListener.
     * @return if it was successful or not
     */
    public boolean addWarriorDeathListener(IWarriorDeathListener listener)
    {
        return this.iwdlList.add(listener);
    }

    /**
     * Clears the Core Death Listeners.
     */
    public void clearWarriorDeathListener()
    {
        this.iwdlList.clear();
    }
}
